/////////////////////////////////////////////////////////////////
// Script:  slcicp prim script
//          - turns prim into whatever cicp hub wants it to be
//          -- this can be a gateway selector (see cicp_start)
//          -- this can be an element of a graph
//
/////////////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////////////
// constants
/////////////////////////////////////////////////////////////////

integer     CICP_PRIM_CHANNEL   = -25973762; // channel for all cicp prims
integer     CICP_BACK_CHANNEL   = -25973770; // channel for responses to cicp init prim

// cell types (non binary)
integer     CELL_NONE           = 1; // cell is nothing special
integer     CELL_ORIGIN         = 2; // cell is the origin of graph (0/0/0)
integer     CELL_LEGEND         = 3; // cell is element of the x axis (1/0/0)

// highlight flags (binary)
integer     HIGHLIGHT_NONE      = 0; // no highlighting
integer     HIGHLIGHT_USER      = 1; // highlighting activated by user (e.g. through touch or menu)
integer     HIGHLIGHT_AXIS_X    = 2; // activated by x-axis-highlighting
integer     HIGHLIGHT_AXIS_Y    = 4; // activated by y-axis-highlighting
integer     HIGHLIGHT_AXIS_Z    = 8; // activated by z-axis-highlighting

// touch constants (see state graphing)
integer     TOUCH_COUNT_RESET   = 3; // reset touchCounter after this number of touches
string      TOUCH_MSG_HIGHLIGHT = "Highlight cell"; // highlight node
string      TOUCH_MSG_MENU      = "Show menu"; // show node menu
string      TOUCH_MSG_CLEAR     = "Remove highlight"; // remove any highlighting and title

// constants to save type of last cicp request for handling the response properly
string      CICP_REQ_DISPATCH   = "dispatch"; // last request was of type "dispatch"
string      CICP_REQ_MENU       = "menu"; // type "menu"
string      CICP_REQ_CHUNK      = "chunk"; // type "chunk"
string      CICP_REQ_MSG        = "msg"; // type "msg"

// constants for internal menu communication
integer     CMD_MENU            = 1; // prefix for specifying the message as menu communication
integer     CMD_ADD_MENU        = 2; // add a menu
integer     CMD_ADD_BUTTON      = 4; // add a button to last added menu
integer     CMD_SHOW_MENU       = 8; // display menu to user and handle user actions
integer     CMD_ADD_NODE        = 16; // add a node to selected nodes list
integer     CMD_REMOVE_NODE     = 32; // remove a node from nodes list
integer     CMD_MENU_CHOICE     = 64; // message contains menu choice by user and the list of selected nodes
integer     CMD_RESET           = 128; // reset scripts in the prim

// graph chat commands (see processGraphChat())
string      CMD_GRAPH_DELETE    = "del"; // graph deletion
string      CMD_GRAPH_RESET_HL  = "reset"; // graph highlight reset
string      CMD_GRAPH_GROUP_ON  = "elev"; // turn on group highlighting
string      CMD_GRAPH_GROUP_OFF = "delev"; // turn off group highlighting

// prim chat commands (see processPrimChat())
string      CMD_PRIM_CICP       = "cicp"; // cicp line
string      CMD_PRIM_CICP_MENU  = "menu"; // cicp menu line
string      CMD_PRIM_CICP_CHUNK = "chunk"; // cicp chunk line
string      CMD_PRIM_PING       = "ping"; // ping
string        CMD_PRIM_PONG        = "pong"; // pong
string      CMD_PRIM_MENU_REQ   = "menureq"; // menu request to menu prim
string      CMD_PRIM_NODE_ADD   = "add"; // add node to selected nodes
string      CMD_PRIM_NODE_REM   = "remove"; // remove node from selected nodes
string      CMD_PRIM_HL_ON      = "high"; // highlight node

// control chat commands (see processControlChat())
string      CMD_CTRL_CONFIG     = "config"; // config data
string      CMD_CTRL_DIE        = "die"; // delete cicp prim
string      CMD_CTRL_RESET      = "reset"; // reset cicp prim

// graph update types, stores if graph should update over time or not (see graphUpdateType)
integer     UPDATE_TYPE_FIXED   = 1; // graph stays fixed
integer     UPDATE_TYPE_INTERVAL= 2; // graph receives updates in intervals

/////////////////////////////////////////////////////////////////
// global vars
/////////////////////////////////////////////////////////////////

integer     primId              = -1; // ID of this prim
integer     primChannel         = 0; // channel for accessing this prim
string        regionName            = ""; // stores region's name as it's often used in cicp requests

list        httpParameters      = [ HTTP_METHOD, "GET",  // default HTTP parameters
                                    HTTP_MIMETYPE, "text/plain;charset=utf-8",
                                    HTTP_VERIFY_CERT, FALSE];
key         currentHttpRequest  = ""; // key of last HTTP request
string      currentRequestType  = ""; // type of last CICP request (see CICP_REQ_-constants)
string      initialPrimChat     = ""; // first prim chat that starts graphing

// config related variables
string      cicpUrl             = ""; // url of cicp hub (config: "cicp_url")
integer     primChannelOffset   = 0; // prim channel offset (config: "prim_channel_offset")
integer     graphChannelOffset  = 0; // graph channel offset (config: "graph_channel_offset")

// prim properties at rez
vector      origPos             = ZERO_VECTOR; // Position at rez
list        origProperties      = [];

// properties set by cicp hub which are applied when displaying a graph
vector      graphNormalColor    = ZERO_VECTOR; // color if prim is not highlighted
vector      graphHighlightColor = ZERO_VECTOR; // color if prims is highlighted
integer     graphId             = -1; // id of graph
vector      graphCellIndex      = ZERO_VECTOR; // relative coordinates of cell in graph
integer     graphUpdateType     = UPDATE_TYPE_FIXED; // update type (default is fixed)
float        graphUpdateInterval = 0.0; // interval to refresh live graph
integer        graphUpdateOn        = TRUE; // turns refreshs on/off
integer     graphChannel        = 0; // (chat) channel of graph
integer     graphCellType       = 0; // cell type (origin/legend/none, see CELL_-constants)
integer     graphMenuPrimId     = 0; // prim id of the menu prim (which processes the cicp menus for this graph)
string      graphNodeMenuId     = ""; // identifier of the menu of this node (e.g. "r", "n", "y", "x", ...)
integer     graphNodeId         = 0; // id of this node in graph
integer     newGraphId          = 0; // no idea yet - ask

// title and highlighting
integer     titleOn             = FALSE; // current display status of title
string      titleText           = ""; // current displayed text in title
vector      titleColor          = <0., 1., 0.>; // color of title
float       titleAlpha          = 0.8; // transparency of title
integer     highlightMask       = 0; // stores source of highlighting as a flag mask (see HIGHLIGHT_-constants)
float       highlightGlow       = 0.4; // glow intensity when highlighted ([0.0;1.0])

integer     touchCounter         = 0; // counts number of touches (to distinguish single/double/triple/...le clicks/touches)

/////////////////////////////////////////////////////////////////
// functions
/////////////////////////////////////////////////////////////////

// processes menu interactions by user
processMenuChoice(string menuChoiceString)
{
    // process normal menu choices
    list menuParameters = llCSV2List(menuChoiceString);
    integer menuOptionId = llList2Integer(menuParameters, 3);
    integer menuSrcPrimId = llList2Integer(menuParameters, 0);
    
    // process negative values first
    if (menuOptionId == -1 || menuOptionId == -4) {
        // highlight prim
        forwardPrimChat(menuSrcPrimId, CMD_PRIM_HL_ON);
    }
    else if (menuOptionId == -2) {
        // reset highlighting in whole graph
        forwardGraphChat(CMD_GRAPH_RESET_HL);
    }
    else if (menuOptionId == -3) {
        // delete whole graph inworld
        forwardGraphChat(CMD_GRAPH_DELETE);
        
        // send cicp graph deletion
        sendGraphDelete();
    }
    else if (menuOptionId == -5) {
        // start/stop refreshing of graph
        // toggle graphUpdateOn
        graphUpdateOn = !graphUpdateOn;
        // and set the timer
        if (graphUpdateOn) {
            llSetTimerEvent(graphUpdateInterval);
        }
        else {
            llSetTimerEvent(0.0);
        }
    }
    else {
        // transmit menu choice and list of selected nodes to cicp hub (= menuChoiceString without first parameter)
        menuChoiceString = llList2CSV(llList2List(menuParameters, 1, -1));
        cicpRequest(CICP_REQ_MSG, "msg=" + menuChoiceString + "&region=" + regionName);
    }
}

// process chat on graph channel
processGraphChat(string message)
{
    // extract command out of parameters list
    list parameters = llCSV2List(message);
    string command = llList2String(parameters, 0);
    parameters = llList2List(parameters, 1, -1);

    if (command == CMD_GRAPH_DELETE) {
        // graph has been deleted, reset this prim
        reset();
    }
    else if (command == CMD_GRAPH_RESET_HL) {
        // reset highlight
        highlightMask = HIGHLIGHT_NONE;
        highlight();
    }
    else if (command == CMD_GRAPH_GROUP_ON || command == CMD_GRAPH_GROUP_OFF) {;
        if (graphCellType != CELL_LEGEND && graphCellType != CELL_ORIGIN) {
            vector axisCellIndices = <llList2Float(parameters, 0), llList2Float(parameters, 1), llList2Float(parameters, 2)>;

            integer highlightBitMask = HIGHLIGHT_NONE;

            // process axes
            if (axisCellIndices.x == graphCellIndex.x) {
                highlightBitMask = highlightBitMask | HIGHLIGHT_AXIS_X;
            }
            else if (axisCellIndices.y == graphCellIndex.y) {
                highlightBitMask = highlightBitMask | HIGHLIGHT_AXIS_Y;
            }
            else if (axisCellIndices.z == graphCellIndex.z) {
                highlightBitMask = highlightBitMask | HIGHLIGHT_AXIS_Z;
            }

            // (un-)highlight the cell
            if (command == CMD_GRAPH_GROUP_ON) {
                setHighlight(highlightBitMask);
            }
            else {
                unsetHighlightFlag(highlightBitMask);
            }
        }
    }
}

// processes data coming from the slcicp main script
processControlChat(string data)
{
    // extract command and parameters
    list parameters = llCSV2List(data);
    string command = llList2String(parameters, 0);
    parameters = llList2List(parameters, 1, -1);

    // process commands
    if (command == CMD_CTRL_CONFIG) {
        // save received config
        saveConfig(parameters);
    }
    else if (command == CMD_CTRL_DIE) {
        sendGraphDelete();
        llDie();
    }
    else if (command == CMD_CTRL_RESET) {
        sendGraphDelete();
        reset();
    }
}

// processes chat on prim channel
processPrimChat(string message)
{
    // probably a pipelined cicp message or
    // other inworld graph action
    // -> extract parameters and process
    list parameters = llCSV2List(message);
    string prefix = llList2String(parameters, 0);
    parameters = llList2List(parameters, 1, -1);

    if (prefix == CMD_PRIM_CICP) {
        // line is a forwarded cicp line
        string command = llList2String(parameters, 0);

        if (command == CMD_PRIM_CICP_MENU) {
            // request menu information from cicp hub
            setGraphId(llList2Integer(parameters, 1));
            cicpRequest(CICP_REQ_MENU, "layer=" + (string)graphId);
        }
        else if (command == CMD_PRIM_CICP_CHUNK) {
            setGraphId(llList2Integer(parameters, 1));
            integer chunkId = llList2Integer(parameters, 2);
            // request graph data from cicp
            cicpRequest(CICP_REQ_CHUNK, "layer=" + (string)graphId +
                        "&chunk=" + (string)chunkId);
        }
        else {
            // line contains prim properties that have to be applied
            configNode(llList2CSV(parameters));
        }
    }
    else {
        if (prefix == CMD_PRIM_PING) {
            // respond to ping with "pong" to slcicp main script
            llRegionSay(CICP_BACK_CHANNEL, CMD_PRIM_PONG);
        }
        else if (prefix == CMD_PRIM_MENU_REQ) {
            // menu request by another cicp prim
            // tell menu script to show menu
            llMessageLinked(LINK_THIS, CMD_MENU | CMD_SHOW_MENU, llList2CSV(parameters) + "," + (string)(primChannel + CICP_PRIM_CHANNEL), "");
        }
        else if (prefix == CMD_PRIM_NODE_ADD) {
            // add another cicp prim to the list of selected nodes
            llMessageLinked(LINK_THIS, CMD_MENU | CMD_ADD_NODE, llList2String(parameters, 0), "");
        }
        else if (prefix == CMD_PRIM_NODE_REM) {
            // remove a cicp prim from the list of selected ndoes
            llMessageLinked(LINK_THIS, CMD_MENU | CMD_REMOVE_NODE, llList2String(parameters, 0), "");
        }
        else if (prefix == CMD_PRIM_HL_ON) {
            // highlighting caused by menu choice of user
            setHighlight(HIGHLIGHT_USER);
        }
        else {
            error("Unknown prefix on prim chat: " + prefix);
        }
    }
}

// processes the cicp response responseBody using magic
processCicpResponse(string responseBody)
{
    newGraphId = 0;
    // split lines for method's line processing
    list lines = llParseString2List(responseBody, ["\n"], []);
    string line = "";
    integer len = llGetListLength(lines);
    integer i;
    integer applyNextLineOnPrim = FALSE; // flag to set if next line contains instructions for prim properties
    string createType = ""; // stores the type of the create command ("new" or "mod")
    integer resetAfterProcessing = FALSE; // flag to set if script should reset after processing last line
    for (i = 0; i < len; ++i) {
        line = llList2String(lines, i);
        
        // determine which method to use by the type of last request
        if (currentRequestType == CICP_REQ_MENU) {
            if (line == "more") {
                cicpRequest(CICP_REQ_MENU, "layer=" + (string)graphId);
            }
            else if (line == "nodes") {
                // next line contains instructions to modify the prim
                applyNextLineOnPrim = TRUE;
            }
            else if (applyNextLineOnPrim) {
                // apply instructions on prim and reset mode variable
                applyNextLineOnPrim = FALSE;
                configNode(line);
            }
            else {
                // process menu declarations
                list lineList = llCSV2List(line);
                string prefix = llList2String(lineList, 0);
                if (prefix == "menu") {
                    // add new menu
                    llMessageLinked(LINK_THIS, CMD_MENU | CMD_ADD_MENU, llList2String(lineList, 1) + "," + (string)graphId, "");
                }
                else if (prefix == "endmenu") {
                    // do nothing
                }
                else {
                    // add new button to last added menu
                    llMessageLinked(LINK_THIS, CMD_MENU | CMD_ADD_BUTTON, llList2String(lineList, 0) + "," + llList2String(lineList, 2), "");

                    // check if this button is a refresh button, because this indicates the graph to be a live graph
                    if (prefix == "-5") {
                        // contains instructions for live graphing
                        graphUpdateType = UPDATE_TYPE_INTERVAL;
                        graphUpdateOn = TRUE;
                        graphUpdateInterval = llList2Float(lineList, 1) * -1; // "positivize" value, since the interval is negative in cicp line
                        // set timer event for graph refreshs
                        llSetTimerEvent(graphUpdateInterval); 
                    }
                }
            }
        }
        else if (currentRequestType == CICP_REQ_CHUNK) {
            // forward cicp line to prim
            forwardCicpLine(line);
        }
        else if (currentRequestType == CICP_REQ_DISPATCH || currentRequestType == CICP_REQ_MSG) {
            if (line == "more") {
                cicpRequest(currentRequestType, "layer=" + (string)newGraphId);
            }
            else if (line == "new") {
                createType = "new";
            }
            else if (line == "mod") {
                createType = "mod";
            }
            else if (llGetSubString(line, 0, 3) == "dump") {
                // producer is providing a dump
                list tempList = llParseString2List(line, [","], []);
                llLoadURL(llGetOwner(), "Cicp Data Dump at ", llList2String(tempList, 1));
            }
            else if (llGetSubString(line, 0, 4) == "error") {
                error("CICP Error: " + llGetSubString(line, 6, -1));
            }
            else if (line != "ok") {
                // check if there's a graph update
                if (createType != "") {
                    list lineList = llParseString2List(line, [";"], []);
                    list subList = llCSV2List(llList2String(lineList, 1));
                    newGraphId = llList2Integer(subList, 1);

                    // process create types
                    if (createType == "new") {
                        if (newGraphId == graphId) {
                            // free all other prims in this graph
                            llRegionSay(graphChannel, CMD_GRAPH_DELETE);
                            // wait for prims to be reset, then continue processing chunk lines
                            llSleep(1.0);
                            // reset prim after processing
                            resetAfterProcessing = TRUE;
                        }
                    }
                    // reset create  type
                    createType = "";
                }
                // pipeline the cicp line to target prim
                forwardCicpLine(line);

                // check if there are instructions for this prim
                // and do not reset it if so
                if (resetAfterProcessing) {
                    integer primTargetId = (integer)llGetSubString(line, 0, llSubStringIndex(line, ","));
                    if (primTargetId == primId) {
                        resetAfterProcessing = FALSE;
                    }
                }
            }
        }
    }

    // process resetting if flag is set
    if (resetAfterProcessing) {
        reset();
    }
}

// handles touches on prim when graphing
handleTouch()
{
    ++touchCounter;
    // process touch
    if (touchCounter == 1) { // single-click: highlighting, title
        // set highlighting
        setHighlight(HIGHLIGHT_USER);
        // toggle title
        setTitle(!titleOn);

        llSetTouchText(TOUCH_MSG_MENU);
    }
    else if (touchCounter == 2)  {
        // collect menu parameters and tell menu prim to show the menu
        list menuParameters = [CMD_PRIM_MENU_REQ, primId, graphNodeId, llDetectedKey(0)
                               , titleText, graphNodeMenuId];

        // forward menu request to menu prim
        forwardPrimChat(graphMenuPrimId, llList2CSV(menuParameters));

        llSetTouchText(TOUCH_MSG_CLEAR);
    }
    else if (touchCounter >= TOUCH_COUNT_RESET) {
        // last touch: reset user highlighting
        unsetHighlightFlag(HIGHLIGHT_USER);
        llSetTouchText(TOUCH_MSG_HIGHLIGHT);
        // reset touch counter
        touchCounter = 0;
    }
}

// forwards chat to the whole graph and to the prim itself
forwardGraphChat(string chatLine)
{
    llRegionSay(graphChannel, chatLine);
    processGraphChat(chatLine);
}

// forwards chat to another cicp prim or processes it internally if target prim is this prim
forwardPrimChat(integer targetPrimId, string chatLine)
{
    // check if chat is meant for this prim or another
    if (targetPrimId != primId) {
        // forward chat to prim
        llRegionSay(getPrimChannel(targetPrimId), chatLine);
    }
    else {
        // process it directly
        processPrimChat(chatLine);
    }
}

// forwards a cicp line to the right prim
forwardCicpLine(string line)
{
    // extract target prim's id and the cicp line
    list parameters = llParseString2List(line, [";"], []);
    integer targetPrimId = llList2Integer(parameters, 0);
    string cicpLine = CMD_PRIM_CICP + "," + llList2String(parameters, 1);

    forwardPrimChat(targetPrimId, cicpLine);
}

// sends a cicp request to hub to address with request type requestType and additional parameters addToUrl
cicpRequest(string requestType, string addToUrl)
{
    setProcessingEffect(TRUE);
    // save requestType
    currentRequestType = requestType;
    // do request to cicp hub
    currentHttpRequest = llHTTPRequest(cicpUrl + "type=" + requestType + "&" + addToUrl, httpParameters, "");
}

// sends a deletion request for this graph to hub
sendGraphDelete()
{
    // to only send one request, this is done by the menu prim
    if (primId == graphMenuPrimId && graphId != -1) {
        // delete graph at cicp hub
        cicpRequest(CICP_REQ_MSG, "msg=" + (string)graphId + "," + (string)graphNodeId + ",-3&region=" + regionName);
    }
}

// reads and stores instructions by CICP hub
// applies them to prim
configNode(string propLine)
{
    // extract properties from line
    list propList = llCSV2List(propLine);

    // read and save properties
    titleText = llUnescapeURL(llList2String(propList, 17));

    vector vparm1 = <llList2Float(propList, 1),
    llList2Float(propList, 2),
    llList2Float(propList, 3)>;
    vector vparm2 = <llList2Float(propList, 4),
    llList2Float(propList, 5),
    llList2Float(propList, 6)>;

    // extract indices and determine celltype (origin or legend)
    graphCellIndex = <llList2Float(propList, 14),
    llList2Float(propList, 15),
    llList2Float(propList, 16)>;

    if (graphCellIndex == ZERO_VECTOR) {
        graphCellType = CELL_ORIGIN;
    }
    else {
        vector normalized = llVecNorm(graphCellIndex);
        if (normalized == <1., 0., 0.> || normalized == <0., 1., 0.> || normalized == <0., 0., 1.>) {
            graphCellType = CELL_LEGEND;
        }
        else {
            graphCellType = CELL_NONE;
        }
    }

    graphNormalColor = <llList2Float(propList, 7),
    llList2Float(propList, 8),
    llList2Float(propList, 9)>;
    graphHighlightColor = < llList2Float(propList, 11),
    llList2Float(propList, 12),
    llList2Float(propList, 13)>;
    float graphFaceAlpha = llList2Float(propList, 10);

    graphNodeMenuId = llList2String(propList, 18);
    graphNodeId = llList2Integer(propList, 19);
    graphMenuPrimId = llList2Integer(propList, 20);
    setGraphId(llList2Integer(propList, 21));

    // extract shape type and position
    vector graphPrimScale = llGetScale(); // size of prim
    string graphPrimShape = llList2String(propList, 0); // shape (box/sphere/cylinder/...)
    vector graphPrimPosition = vparm1; // position in graph coordinates
    rotation graphPrimRotation = llGetRot(); // prim rotation (is calculated differently by prim shapes)

    // adjust values by prim shape and apply properties on prim
    list primParams = [];
    if (graphPrimShape == "sph") { //
        float radius = llList2Float(propList, 5) * 2;
        graphPrimScale = <radius, radius, radius>;

        primParams = [PRIM_TYPE, PRIM_TYPE_SPHERE, 0, <0.0, 1.0, 0.0>,
                      0.0, <0.0, 0.0, 0.0>, <0.0, 1.0, 0.0>];
    }
    else if (graphPrimShape == "lin") {
        graphPrimPosition = vparm1 + ((vparm2 - vparm1) / 2);
        graphPrimScale = <.02, .02, llVecDist(vparm1, vparm2)>;
        graphPrimRotation = llRotBetween(<0, 0, 1>, llVecNorm(vparm2 - vparm1));

        primParams = [PRIM_TYPE, PRIM_TYPE_CYLINDER, 0, <0.0, 1.0, 0.0>,
                      0.0, <0.0, 0.0, 0.0>, <1.0, 1.0, 0.0>, <0.0, 0.0, 0.0>];
    }
    else {
        float cellSize = llList2Float(propList, 4) * 2;
        float height = llList2Float(propList, 5);

        graphPrimPosition.z += height / 2;

        // adjust height to be positive
        if (height < 0) {
            height *= -1;
        }

        graphPrimScale = <cellSize, cellSize, height>;
        primParams = [PRIM_TYPE, PRIM_TYPE_BOX, 0, <0.0, 1.0, 0.0>,
                      0.0, <0.0, 0.0, 0.0>, <1.0, 1.0, 0.0>, <0.0, 0.0, 0.0>];
    }

    // set prim properties
    llSetPrimitiveParams(primParams + [PRIM_COLOR, ALL_SIDES, graphNormalColor, graphFaceAlpha,
                                       PRIM_SIZE, graphPrimScale,
                                       PRIM_ROTATION, graphPrimRotation]);
    llSetObjectDesc(titleText);

    // move to position
    warpPos(graphPrimPosition);
}

// sets new graph id and updates listen on graphchannel
setGraphId(integer newId)
{
    graphId = newId;
    // start graph listen
    graphChannel = -(graphChannelOffset + graphId);
    llListen(graphChannel, "", NULL_KEY, "");
}

// move prim to position destpos bypassing 10m-limit by setting multiple PRIM_POSITION
warpPos(vector destpos)
{   //R&D by Keknehv Psaltery, 05/25/2006
    //with a little poking by Strife, and a bit more
    //some more munging by Talarus Luan
    //Final cleanup by Keknehv Psaltery
    //Changed jump value to 411 (4096 ceiling) by Jesse Barnett
    // Compute the number of jumps necessary
    integer jumps = (integer)(llVecDist(destpos, llGetPos()) / 10.0) + 1;
    // Try and avoid stack/heap collisions
    if (jumps > 411)
        jumps = 411;
    list rules = [ PRIM_POSITION, destpos ];  //The start for the rules list
    integer count = 1;
    while ( ( count = count << 1 ) < jumps)
        rules = (rules=[]) + rules + rules;   //should tighten memory use.
    llSetPrimitiveParams( rules + llList2List( rules, (count - jumps) << 1, count) );
    if ( llVecDist( llGetPos(), destpos ) > .001 ) //Failsafe
        while ( --jumps )
            llSetPos( destpos );
}

// set/unset effect showed while processing a user choice
setProcessingEffect(integer onOff)
{
    if (onOff) {
        // turn on effect
        llSetText("Working...", <1., 0.5, 0.5>, 1.);
    }
    else {
        // turn off effect
        llSetText("", ZERO_VECTOR, 0.);
        // restore title
        setTitle(titleOn);
    }
}

// display/hide title
setTitle(integer onOff)
{
    // set title on/off
    llSetText(titleText, titleColor, titleAlpha * onOff);

    // save title state
    titleOn = onOff;
    string addOrRemoveBit = CMD_PRIM_NODE_ADD;
    if (!titleOn) {
        addOrRemoveBit = CMD_PRIM_NODE_REM;
    }
    // add/remove prim to/from list of selected nodes
    forwardPrimChat(graphMenuPrimId, addOrRemoveBit + "," + (string)graphNodeId);
}

// unsets the highlight flag hlFlag and updates prim
unsetHighlightFlag(integer hlFlag)
{
    highlightMask = (highlightMask | hlFlag) & ~hlFlag;
    highlight();
}

// sets the highlight flag hlFlag and updates prim
setHighlight(integer hlFlag)
{
    highlightMask = highlightMask | hlFlag;
    highlight();
}

// sets the highlight flag hlFlag and highlights prim accordingly
// see HIGHLIGHT_-constants
highlight()
{
    list primParams = [];
    integer highlightState = TRUE;
    if (highlightMask == HIGHLIGHT_NONE) {
        primParams = [PRIM_GLOW, ALL_SIDES, 0.0,
                      PRIM_COLOR, ALL_SIDES, graphNormalColor, 1.];
        highlightState = FALSE;
    }
    else {
        primParams = [PRIM_GLOW, ALL_SIDES, highlightGlow,
                      PRIM_COLOR, ALL_SIDES, graphHighlightColor, 1.];
        highlightState = TRUE;
    }

    // customize prim properties
    llSetPrimitiveParams(primParams);

    // propagate highlight to row
    if (graphCellType == CELL_LEGEND) {
        // get node index as "x,y,z" string
        string groupHighlightString = llGetSubString((string)graphCellIndex, 1, -2);
        // add group highlighting on/off depending on highlight state on/off
        if (highlightState) {
            groupHighlightString = CMD_GRAPH_GROUP_ON + "," + groupHighlightString;
        }
        else {
            groupHighlightString = CMD_GRAPH_GROUP_OFF + "," + groupHighlightString;
        }
        forwardGraphChat(groupHighlightString);
    }
}

// returns the channel of the cicp prim with id primId
integer getPrimChannel(integer cicpPrimId)
{
    return -(cicpPrimId + primChannelOffset);
}

// replaces current config with new config configuration
saveConfig(list configuration)
{
    list variables = llList2ListStrided(configuration, 0, -1, 2);

    // set config related variables
    cicpUrl = llList2String(configuration, (llListFindList(variables, ["cicp_url"]) << 1) + 1);
    graphChannelOffset = llList2Integer(configuration, (llListFindList(variables, ["graph_channel_offset"]) << 1) + 1);
    primChannelOffset = llList2Integer(configuration, (llListFindList(variables, ["prim_channel_offset"]) << 1) + 1);
    primChannel = -(primChannelOffset + primId);
    httpParameters = llListReplaceList(httpParameters, [llList2Integer(configuration, (llListFindList(variables, ["ssl"]) << 1) + 1)], 5, 5);
}

reset()
{
    // reset prim properties to original values
    llSetPrimitiveParams(origProperties);

    // move to original pos
    warpPos(origPos);

    // save primId and config in object's description to be able to restore it after reset
    string configString = (string)primId + ",cicp_url," + cicpUrl + ",prim_channel_offset," + (string)primChannelOffset + ",graph_channel_offset," + (string)graphChannelOffset;
    llSetObjectDesc(configString);

    // reset script to free memory
    llResetScript();
}

savePrimProperties()
{
    // save important properties at rez
    origPos = llGetPos();
    origProperties = [PRIM_SIZE, llGetScale()];
    origProperties += [PRIM_TYPE] + llGetPrimitiveParams([PRIM_TYPE]);
    origProperties += [PRIM_ROTATION] + [llGetRot()];
    origProperties += [PRIM_GLOW, ALL_SIDES] + [llList2Float(llGetPrimitiveParams([PRIM_GLOW, ALL_SIDES]), 0)];
    origProperties += [PRIM_COLOR, ALL_SIDES, llGetColor(ALL_SIDES), llGetAlpha(ALL_SIDES)];
}

error(string msg)
{
    // output error message to user
    llOwnerSay("[CICP Prim " + (string)primId + "] Error: " + msg);
}

/////////////////////////////////////////////////////////////////
//  states: default, waitForConfig, idle, graphing
/////////////////////////////////////////////////////////////////

 default
 // initiates prim config and changes to idle state
 // additionally changes to waitForConfig state if prim has been rezzed to wait for configuration
 {
     state_entry()
     {
         // save prim properties
         savePrimProperties();

         // save region name
         regionName = llGetRegionName();

         // read config from object's description if there is config (necessary if script was reset)
         if (primId == -1 && llGetObjectDesc() != "") { // using primId's default value to check if prim has been rezzed or just reset
             list config = llCSV2List(llGetObjectDesc());
             primId = llList2Integer(config, 0);
             saveConfig(llList2List(config, 1, -1));
         }

         state idle;
     }
 }

state waitForConfig
// prim has been rezzed and is awaiting config
{
    state_entry()
    {
        // start listening for config
        llListen(CICP_PRIM_CHANNEL, "", NULL_KEY, "");
    }

    listen(integer channel, string name, key id, string msg)
    {
        if (channel == CICP_PRIM_CHANNEL) {
            processControlChat(msg);
            // check if cicp url has been configured
            if (cicpUrl != "") {
                llSetObjectDesc("");
                state default;
            }
        }
    }
}

state idle
// script is configured and initiated, wait for actions
{
    state_entry()
    {
        // start listens
        llListen(primChannel, "", NULL_KEY, "");
        llListen(CICP_PRIM_CHANNEL, "", NULL_KEY, "");

        // tell menu script to reset
        llMessageLinked(LINK_THIS, CMD_MENU | CMD_RESET, "", "");

        // clear title and effects
        llSetText("", ZERO_VECTOR, 1.);
    }

    on_rez(integer pId)
    {
        // save rez parameter as primId
        primId = pId;
        // change to waitForConfig to receive actual config from slcicp main prim
        state waitForConfig;
    }

    listen(integer channel, string name, key id, string msg)
    {
        if (channel == CICP_PRIM_CHANNEL) {
            processControlChat(msg);
        }
        else if (channel == primChannel) {
            // initial prim chat activates graphing state
            // save initial prim chat so graphing state can process it
            initialPrimChat = msg;
            state graphing;
        }
    }
}

state graphing
// prim is part of a graph
{
    state_entry()
    {
        // start listens
        llListen(CICP_PRIM_CHANNEL, "", NULL_KEY, "");
        llListen(primChannel, "", NULL_KEY, "");

        // set touch text
        llSetTouchText(TOUCH_MSG_HIGHLIGHT);

        // reset object description
        llSetObjectDesc("");

        // process initial prim chat
        processPrimChat(initialPrimChat);
        // free memory
        initialPrimChat = "";
    }

    listen(integer channel, string name, key id, string msg)
    {
        if (channel == graphChannel) {
            processGraphChat(msg);
        }
        else if (channel == primChannel) {
            processPrimChat(msg);
        }
        else if (channel == CICP_PRIM_CHANNEL) {
            processControlChat(msg);
        }
    }

    timer()
    {
        // check if graph is a live graph and perform refresh
        if (graphUpdateType == UPDATE_TYPE_INTERVAL) {
            // request refresh at cicp hub
            cicpRequest(CICP_REQ_MSG, "msg=" + (string)graphId + "," + (string)graphNodeId + ",-5&region=" + regionName);
        }
    }

    touch_start(integer n)
    {
        handleTouch();
    }

    link_message(integer linkNumber, integer number, string data, key id)
    {
        if (number & CMD_MENU) {
            if (number & CMD_MENU_CHOICE) {
                processMenuChoice(data);
            }
        }
    }

    http_response(key requestId, integer statusCode, list data, string body)
    {
        if (requestId == currentHttpRequest) {
            processCicpResponse(body);
            setProcessingEffect(FALSE);
        }
    }
}
